﻿using System;
using System.Collections.Concurrent;
using System.Diagnostics.Contracts;
using Wombat.Core.Factories;

namespace Wombat.Core.Services
{
    public class TagStateService : ITagStateService
    {
        private struct TagStateData
        {
            public DateTime Time1;
            public object Value1;
            public DateTime Time2;
            public object Value2;
            private bool _filled;

            public TagStateData(DateTime time, object value)
            {
                Time1 = time;
                Value1 = value;
                _filled = false;
                Time2 = default(DateTime);
                Value2 = null;
            }

            public void Add(DateTime time, object value)
            {
                if (_filled)
                {
                    Time1 = Time2;
                    Value1 = Value2;
                }
                Time2 = time;
                Value2 = value;
                _filled = true;
                
                //swap the times to always be in the correct order
                if (Time1 < Time2)
                {
                    var tTime = Time1;
                    var tval = Value1;
                    Time1 = Time2;
                    Value1 = Value2;
                    Time2 = tTime;
                    Value2 = tval;
                }
            }
        }

        private readonly ITagRpository _tagRpository;
        private readonly ISpecialValueFactory _specialValueFactory;

        private readonly ConcurrentDictionary<Guid, TagStateData> _stateRepository = 
            new ConcurrentDictionary<Guid, TagStateData>();

        public TagStateService(ITagRpository tagRpository, ISpecialValueFactory specialValueFactory)
        {
            _tagRpository = tagRpository;
            _specialValueFactory = specialValueFactory;
        }

        public bool TryGetTagValueAt(Guid tagId, DateTime time, out object tagValue)
        {
            tagValue = null;
            if (!_stateRepository.ContainsKey(tagId)) return false;
            var tsd = _stateRepository[tagId];
            var tag = _tagRpository.Get(tagId);
            var corruptVal = _specialValueFactory.GetCorruptValue(tag.DataType);

            if (tag.DataStreamType == TagDataStreamType.Descrete)
            {
                tagValue = tsd.Value1;
            }
            else
            {
                if (tsd.Value1 == null || tsd.Value2 == null || tsd.Value1 == corruptVal)
                {
                    tagValue = null;
                    return false;
                }
                if (tsd.Value2 == corruptVal)
                {
                    tagValue = corruptVal;
                    return false;
                }
                if (!(tsd.Value1 is Double || tsd.Value2 is Double))
                    throw new InvalidOperationException(string.Format("If tag data stream is Continues, only double values are allowed, but value {0} or {1} is not Double.", tsd.Value1, tsd.Value2));
                tagValue = Interpolate(tsd.Time1, (Double)tsd.Value1, tsd.Time2, (Double)tsd.Value2, time);
            }

            return tagValue != corruptVal;
        }

        private static object Interpolate(DateTime time1, Double value1, DateTime time2, Double value2, DateTime time)
        {
            //TODO: Find a solution for the problem of late arriving data!
            //if (time < time2 || time < time1)
                //throw new InvalidOperationException(string.Format(
                //    "TagStateService is only designed to return future values." +
                //    " Currenly there is some data for {0} and {1}, and the requested time is {2} which is less than existing state.", time1, time2, time));
            var m = (value2 - value1)/(time2 - time1).Ticks;
            return (time - time1).Ticks*m + value1;
        }

        /// <summary>
        /// Sets the tag value.
        /// If a value is already set for the given time, this method does nothing.
        /// </summary>
        /// <param name="tagId">The tag id.</param>
        /// <param name="time">The time.</param>
        /// <param name="tagValue">The tag value.</param>
        public void SetTagValue(Guid tagId, DateTime time, object tagValue)
        {
            Contract.Assert(tagId != null && tagId != Guid.Empty);
            Contract.Assert(time != default(DateTime));

            _stateRepository.AddOrUpdate(tagId,
                                         new TagStateData(time, tagValue),
                                         (id, tsd) => UpdateTagStateData(tagId, tsd, time, tagValue));
        }

        private TagStateData UpdateTagStateData(Guid tagId, TagStateData tsd, DateTime time, object tagValue)
        {
            var tag = _tagRpository.Get(tagId);
            if (tag.DataStreamType == TagDataStreamType.Descrete)
            {
                tsd.Time1 = time;
                tsd.Value1 = tagValue;
            }
            else
            {
                if (tsd.Time1 != time && tsd.Time2 != time)
                    tsd.Add(time, tagValue);
            }
            return tsd;
        }

        public void CorruptTagValue(Guid tagId, DateTime time)
        {
            Contract.Assert(tagId != null && tagId != Guid.Empty);
            Contract.Assert(time != default(DateTime));

            var corruptValue = _specialValueFactory.GetCorruptValue(_tagRpository.Get(tagId).DataType);
            SetTagValue(tagId, time, corruptValue);
        }
    }
}